package com.csthink.common.infrastructure.aspect;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.*;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author <a href="mailto:security.2009@live.cn">Mars</a>
 * @since 2023-09-12
 */
public class AbstractSpelAspect implements BeanFactoryAware {

    private final Logger logger0 = LoggerFactory.getLogger(this.getClass());

    protected final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    private BeanFactory beanFactory;

    private static final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();

    private boolean enableExpressionCache;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Value("${common.aspect.spel.expression.cached:true}")
    public void setEnableExpressionCache(boolean enable) {
        this.enableExpressionCache = enable;
    }

    /**
     * Construct EvaluationContext with jp's args, all args in jp will be set in
     * EvaluationContext, using it's parameterName. We can use '#parameterName'
     * in spEL.
     * Using BeanFactoryResolver, so we can use '@beanId' to get bean from spring
     * Context.
     */
    protected EvaluationContext getEvalContext(JoinPoint joinPoint, boolean requireSpringBean) {
        StandardEvaluationContext evalContext = new StandardEvaluationContext(joinPoint.getArgs());
        if (requireSpringBean) {
            // Inject beanFactory get support @beanId
            evalContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
        }
        Object[] args = joinPoint.getArgs();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = methodSignature.getMethod();
        ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
        String[] parametersName = parameterNameDiscoverer.getParameterNames(targetMethod);

        if (args == null || args.length <= 0 ||
                parametersName == null || parametersName.length != args.length) {
            return evalContext;
        }
        // make #arg & #p{0,1,...,n} usable
        for (int i = 0; i < args.length; i++) {
            evalContext.setVariable(parametersName[i], args[i]);
            evalContext.setVariable("p" + i, args[i]);
        }
        return evalContext;
    }

    /**
     * Take spel as template expression, which means, only string within #{} will
     * be considered as spel, others is just pure string.
     */
    protected <T> T evalTemplateSpel(String spel, EvaluationContext context, Class<T> clazz) {
        return evalSpel(spel, context, true, clazz);
    }

    /**
     * Consider all spel is valid spel expression.
     */
    protected <T> T evalSpel(String spel, EvaluationContext context, Class<T> clazz) {
        return evalSpel(spel, context, false, clazz);
    }

    /**
     * Eval both pure or template spel expression
     * <p>
     * If is template, only string within #{} will be considered as spel.
     *
     * @param spel       spel expression
     * @param isTemplate is template expression or not
     * @return
     */
    protected <T> T evalSpel(String spel, EvaluationContext context, boolean isTemplate, Class<T> clazz) {
        if (StringUtils.isEmpty(spel))
            return null;
        T result = null;
        try {
            Expression exp = parseExpression(spel, isTemplate);
            result = exp.getValue(context, clazz);
        } catch (ParseException e) {
            logger0.warn("Fail to parse {E}", spel, e);
        } catch (Exception e) {
            logger0.warn("Fail to eval {E}", spel, e);
        }
        return result;
    }

    private Expression parseExpression(String spel, boolean isTemplate) {
        if (enableExpressionCache) {
            return expressionCache.computeIfAbsent(spel, s -> parseExpression0(s, isTemplate));
        } else {
            return parseExpression0(spel, isTemplate);
        }
    }

    private Expression parseExpression0(String spel, boolean isTemplate) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp;
        if (isTemplate)
            exp = parser.parseExpression(spel, ParserContext.TEMPLATE_EXPRESSION);
        else
            exp = parser.parseExpression(spel);
        return exp;
    }
}
